﻿namespace V8Bridge.FSharp

open System
open System.IO
open System.Reflection
open System.Threading
open V8Bridge.Interface

type ScriptEngineResult =
    | Success of string
    | Fail of string

/// The ScriptEngine executes javascript code in the underlying V8 or Jurassic
/// engine, ensuring that the underlying engine always runs on the same thread,
/// and only executes one thing at a time. That's just how V8 rolls, but we 
/// forgive them because it's so dang fast!
[<RequireQualifiedAccess>]
module ScriptEngine =
    
    type private CompilerAction =
        | InitializeLibrary of string * AsyncReplyChannel<ScriptEngineResult>
        | CallFunction of string * string * AsyncReplyChannel<ScriptEngineResult>

    let private getCompilerType () =
        let suffix = if Environment.Is64BitProcess then "amd64" else "x86"
        let assemblyName = sprintf "V8Bridge.%s.dll" suffix
        let v8Path = Path.Combine(Path.GetTempPath(), assemblyName)
        try
            use output = File.OpenWrite(v8Path)
            use input = Assembly.GetExecutingAssembly().GetManifestResourceStream(assemblyName)
            input.CopyTo output
        with
        | :? IOException | :? UnauthorizedAccessException -> ()

        try
            let v8Interface = typeof<IV8ScriptCompiler>
            let v8Assembly = Assembly.LoadFile(v8Path)
            v8Assembly.GetTypes()
            |> Array.tryFind (fun t -> v8Interface.IsAssignableFrom(t))
            |> function Some t -> t | None -> typeof<JurassicCompiler>
        with _ ->
            eprintf "*** WARNING: You're on ARM, Mono, Itanium (heaven help you), or another architecture\n\
                     which isn't x86/amd64 on NT. Loading the Jurassic compiler, which is much slower."
            typeof<JurassicCompiler>

    let private createCompilerInstance t =
        Activator.CreateInstance(t:Type) :?> IV8ScriptCompiler

    let private agent = MailboxProcessor.Start(fun inbox ->
        let context = new AffinitySyncContext()
        
        let rec loop (compiler: IV8ScriptCompiler option) = async {
            let! msg = inbox.Receive()
            do! Async.SwitchToContext context
            let compiler = 
                match compiler with
                | None -> getCompilerType() |> createCompilerInstance
                | Some c -> c
            match msg with
            | InitializeLibrary(code, reply) ->
                try
                    compiler.InitializeLibrary(code)
                    reply.Reply(Success "")
                with ex ->
                    reply.Reply(Fail ex.Message)
                
            | CallFunction(name, input, reply) ->
                try
                    let output = compiler.Compile(name, input)
                    if output = "undefined" then
                        // This seems to come up randomly for certain files.
                        // For now, retrying.
                        inbox.Post msg
                    elif output.StartsWith "Error:" then
                        reply.Reply(Fail output.[7..])
                    elif output.StartsWith "TypeError:" then
                        reply.Reply(Fail output.[11..])
                    else
                        reply.Reply(Success output)
                with ex ->
                    reply.Reply(Fail ex.Message)

            return! loop (Some compiler)
        }

        loop None
    )

    let initializeLibrary code = async {
        let! result = agent.PostAndAsyncReply(fun reply -> InitializeLibrary(code, reply))
        match result with
        | Success _ -> ()
        | Fail msg  -> invalidOp msg
    }
    
    let callFunction name input =
        agent.PostAndAsyncReply(fun reply -> CallFunction(name, input, reply)) 
